(function() { let scriptLoader = null; let domain = ''; let shop = ''; try { const url = new URL(location.href); domain = new URL(document.currentScript.src).origin; shop = url.pathname.match(/^\/shop\d+/)?.[0].replaceAll('/', '') ?? ''; } catch(e) { } (function(){ // ===== 설정 ===== const HOST_SELECTOR = 'body'; const IFRAME_SELECTOR = "iframe[id^='review_widget']"; const CLASS_SELECTOR = "snap_widget"; // 제거/재부착 배치를 묶을 디바운스 시간 const DEBOUNCE_MS = 150; // ===== 유틸 ===== const getHost = () => document.querySelector(HOST_SELECTOR); const isOurs = (n) => n?.nodeType === 1 && (n.matches?.(IFRAME_SELECTOR) || n.matches?.(CLASS_SELECTOR)); const findAllIn = (root) => { const iframes = Array.from((root || document).querySelectorAll?.(IFRAME_SELECTOR) || []); const classElements = Array.from((root || document).querySelectorAll?.(`.${CLASS_SELECTOR}`) || []); return [...iframes, ...classElements]; } function collectOurIframes(node) { const list = []; if (node?.nodeType === 1) { if (isOurs(node)) list.push(node); if (node.classList.contains(CLASS_SELECTOR)) list.push(node); node.querySelectorAll?.(IFRAME_SELECTOR)?.forEach(n => list.push(n)); if (!list.length) node.querySelectorAll?.(`.${CLASS_SELECTOR}`)?.forEach(n => list.push(n)); } return list; } // ===== 상태 ===== // 제거된 것들 (id 기반 추적) 재부착을 모두 확인하면 로드 const removedIds = new Set(); // 방금 추가된 것들 (노드 자체) const addedNodes = new Set(); // 로드 폭주 방지 let lastLoadAt = 0; // 디바운서 let flushTimer = null; function scheduleFlush() { if (flushTimer) clearTimeout(flushTimer); flushTimer = setTimeout(flush, DEBOUNCE_MS); } function now() { return Date.now(); } // 모든 제거 대상이 다시 붙었는지 확인 function allRemovedBackIn() { if (!removedIds.size) return false; const host = getHost(); if (!host) return false; // host 안의 모든 우리 iframe의 data-snap-id를 수집 const idsInHost = new Set( findAllIn(host).map((el) => el.id || '') ); // removedIds의 모든 id가 host 안에 존재하면 true for (const id of removedIds) { if (!idsInHost.has(id)) return false; } return true; } function readyToLoad() { // 조건: // 1) 뭔가 제거가 있었고(removedIds.size > 0) // 2) 그리고 그들이 전부 다시 붙었음(allRemovedBackIn()) // 3) 혹은, 제거가 있었고 + 방금 추가된 것도 존재(추가/제거 배치가 끝났다고 추정) // 4) 그리고 host 안에 최소 1개라도 존재(실수로 전부 사라진 상태에서 호출 방지) const host = getHost(); const anyInHost = host && findAllIn(host).length > 0; if (!anyInHost) return false; if (removedIds.size === 0) return false; // 엄격 모드: 전부 복귀했는지 if (allRemovedBackIn()) return true; return false; } function flush() { flushTimer = null; if (!readyToLoad()) { // 아직 덜 붙었으면 다음 변화를 기다림 return; } // 로드 폭주/중복 방지 (예: 500ms 쿨다운) if (now() - lastLoadAt < 500) { // 살짝 늦춰서 한 번 더 체크 scheduleFlush(); return; } try { const snapInstance = new window.snapSolution(); const dependent = window.snapinstancedependent; const independent = window.snapinstanceindependent; // 종속, 비종속 위젯 유무 1 : 종속, 2 : 비종속 let widgetPurpose = 0; for (const node of addedNodes) { if (!dependent && !independent) { if (Number(node.id) > 999) { // 비종속 위젯일 때, widgetPurpose = 2; } else { // 종속 위젯일 때, widgetPurpose = 1; } } if (isOurs(node) && !node.classList.contains(CLASS_SELECTOR)) { node.remove(); } } if (dependent) dependent.init(); if (independent) independent.init(); if (widgetPurpose > 0) { // 종속 위젯, dependent 인스턴스값이 존재 하지 않으면, widgetPurpose === 1 && snapInstance.loadScript('review_dependent'); // 비종속 위젯, independent 인스턴스값이 존재 하지 않으면, widgetPurpose === 2 &&snapInstance.loadScript('review_independent'); } } catch (e) { console.error(e); } finally { mo.disconnect(); lastLoadAt = now(); removedIds.clear(); addedNodes.clear(); } } // ===== 옵저버 ===== const mo = new MutationObserver((mutations) => { let touched = false; for (const m of mutations) { // 제거된 노드에서 우리 iframe들 수집 if (m.removedNodes?.length) { Array.from(m.removedNodes).forEach((n) => { const ours = collectOurIframes(n); ours.forEach((iframe) => { // 고유 id 우선순위: data-snap-id > id > name > (fallback) src const id = iframe.getAttribute('id') || iframe.getAttribute('name') || iframe.getAttribute('src') || // 클론 시 src가 유지되는 경우도 많음 ''; // full-popup 위젯은 제외 (상세, 작성...) const ifp = iframe.getAttribute("data-ifp") ?? "0"; if (id && ifp !== "1") { removedIds.add(id); } }); }); } // 추가된 노드에서 우리 iframe들 수집 if (m.addedNodes?.length) { Array.from(m.addedNodes).forEach((n) => { const ours = collectOurIframes(n); ours.forEach((item) => { if (removedIds.has(item.id)) { item.parentNode.classList.remove("loaded"); const host = getHost(); if (host && item.isConnected && host.contains(item)) { addedNodes.add(item); } } else { if (item.classList.contains(CLASS_SELECTOR)) { const host = getHost(); if (host && host.contains(item)) { removedIds.add(item.id); addedNodes.add(item); } } } }); if (addedNodes.size > 0) { touched = true; } }); } } if (touched) { scheduleFlush(); } }); // 문서 전체 감시 mo.observe(getHost(), { childList: true, subtree: true }); // 1분이 지나면 observer 파괴 setTimeout(() => { mo.disconnect(); }, 60000); })(); window.snapSolution = window.snapSolution || function(storeName) { this.onLoadSnapScript = function(url, callback) { let script = document.createElement("script"); script.type = "text/javascript"; script.charset = "utf-8"; script.async = true; if(script.readyState) { // IE 경우 script.onreadystatechange = function() { if(script.readyState === "loaded" || script.readyState === "complete") { script.onreadystatechange = null; typeof callback == 'function' && callback(); } }; } else { // 타 브라우저 script.onload = function() { typeof callback == 'function' && callback(); }; } script.src = url; document.head.appendChild(script); console.log("load : " + url); } this.loadScript = function(solution, callback) { if(!['review_dependent', 'review_independent'].includes(solution)) return; scriptLoader ??= this.getScripts(); scriptLoader .then((response) => { this.onLoadSnapScript(response[solution], callback); }) .catch((e) => { console.log(e); }); } this.getScripts = function() { return new Promise(async (resolve, reject) => { // [SR-5544] 카페24 정책 대응 const abort = new AbortController(); const ms = 15000; const timer = setTimeout(() => abort.abort(), ms); try { let url = `${domain}/script?shop=${shop ?? ''}`; let response; response = await fetch(url, { signal: abort.signal }); response = await response.json(); response?.["message"] && console.log(`[SNAP REVIEW]\n${response?.["message"]}`); // main script load this.onLoadSnapScript(response['main'], () => { snaputil = new snapUtils(response['domain']); // iOS 15버전 이하일 때만 페이지 뒤로가기 시 iframe reload 해주도록 변경 if(snaputil.isIOS() && snaputil.getVersionIOS() < 15) { let widgets = document.querySelectorAll('iframe[name^="review_widget"]'); widgets.forEach(widget => snaputil.reload_iframe(widget)); } return resolve(response); }); } catch (e) { if (e?.name === "AbortError") { throw new Error(`Timeout after ${ms}ms`); } return reject(e); } finally { clearTimeout(timer); } }); } } })();